**Интеграционные Тесты Для Своего ASP.NET Core API**

Интеграционный тест для API проверяет не отдельный метод класса, а реальный HTTP-запрос к твоему приложению.

То есть тест делает почти то же, что:
- браузер
- Postman
- Swagger
- фронтенд

Но делает это программно.

---

**Что Именно Проверяет Такой Тест**
Обычно:
- маршрут находится
- контроллер вызывается
- модель запроса биндингится
- DI работает
- сервисы зарегистрированы
- middleware отрабатывает
- сериализация JSON работает
- возвращается правильный статус-код
- возвращается правильное тело ответа

---

**Чем Он Отличается От Unit-Теста**
Unit:
- вызывает метод напрямую
- не делает HTTP-запрос
- не поднимает приложение

Integration:
- поднимает тестовую версию приложения
- создаёт `HttpClient`
- отправляет реальный HTTP-запрос в твой API
- получает реальный `HttpResponseMessage`

---

## **Как Это Работает**

В ASP.NET Core для этого обычно используют:
- `Microsoft.AspNetCore.Mvc.Testing`
- `WebApplicationFactory<TEntryPoint>`

Идея такая:
1. тест поднимает твоё приложение in-memory
2. создаёт `HttpClient`
3. делает запросы вроде `GET /api/tasks/1`
4. получает ответ как настоящий клиент

То есть без реального браузера и без внешнего сервера.

---

## **Как Выглядит Поток**
Тест делает:

```csharp
var response = await client.GetAsync("/api/tasks/1");
```

Дальше внутри происходит:
1. запрос попадает в pipeline ASP.NET Core
2. middleware обрабатываются
3. роутинг выбирает контроллер
4. контроллер вызывает сервис
5. формируется `ActionResult`
6. ASP.NET сериализует ответ в JSON
7. тест получает `HttpResponseMessage`

---

## **Что Нужно Подключить**
Обычно в тестовый проект добавляют пакет:

```bash
dotnet add package Microsoft.AspNetCore.Mvc.Testing
```

---

## **Базовая Структура**

Допустим, у тебя есть API-приложение `MyApi`.

Простейший integration-тест:

```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;

public class TasksApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public TasksApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task Get_Unknown_Route_Returns_NotFound()
    {
        var response = await _client.GetAsync("/api/unknown");

        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
}
```

---

# **От Простого К Сложному**

## **Уровень 1. Просто проверить статус-код**

### Пример: `GET` возвращает `404`

```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;

public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public UsersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetUser_ReturnsNotFound_WhenUserDoesNotExist()
    {
        var response = await _client.GetAsync("/api/users/999");

        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
}
```

Что здесь проверяется:
- маршрут существует
- приложение поднялось
- запрос реально дошёл до API
- ответ реально `404`

---

## **Уровень 2. Отправить POST с JSON**

### Пример: создание сущности

```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;

public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public UsersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateUser_ReturnsCreated_WhenRequestIsValid()
    {
        var request = new
        {
            name = "Anton"
        };

        var response = await _client.PostAsJsonAsync("/api/users", request);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    }
}
```

Что важно:
- `PostAsJsonAsync` сам сериализует объект в JSON
- тест отправляет настоящий `POST`
- сервер принимает JSON как обычный HTTP body

---

## **Уровень 3. Прочитать JSON-ответ**

### Пример: проверить тело ответа

```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;

public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public UsersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateUser_ReturnsCreatedUser()
    {
        var request = new
        {
            name = "Anton"
        };

        var response = await _client.PostAsJsonAsync("/api/users", request);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);

        var body = await response.Content.ReadFromJsonAsync<UserResponse>();

        Assert.NotNull(body);
        Assert.Equal("Anton", body!.Name);
        Assert.True(body.Id > 0);
    }
}

public class UserResponse
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}
```

Что здесь проверяется:
- и статус
- и JSON
- и правильность сериализации ответа

---

## **Уровень 4. Проверить полный сценарий**

### Пример: создать, потом получить

```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;

public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public UsersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateThenGet_ReturnsSameUser()
    {
        var createResponse = await _client.PostAsJsonAsync("/api/users", new
        {
            name = "Anton"
        });

        Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);

        var createdUser = await createResponse.Content.ReadFromJsonAsync<UserResponse>();
        Assert.NotNull(createdUser);

        var getResponse = await _client.GetAsync($"/api/users/{createdUser!.Id}");

        Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);

        var fetchedUser = await getResponse.Content.ReadFromJsonAsync<UserResponse>();
        Assert.NotNull(fetchedUser);
        Assert.Equal(createdUser.Id, fetchedUser!.Id);
        Assert.Equal("Anton", fetchedUser.Name);
    }
}
```

Это уже хороший интеграционный сценарий.

---

## **Уровень 5. Проверить ошибку валидации**

### Пример: невалидный запрос

```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;

public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public UsersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateUser_ReturnsBadRequest_WhenNameIsEmpty()
    {
        var response = await _client.PostAsJsonAsync("/api/users", new
        {
            name = ""
        });

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }
}
```

Это особенно важно для API.

---

## **Уровень 6. Проверить JSON ошибки**

Если у тебя есть middleware глобальной обработки ошибок, можно проверить и тело ошибки.

```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;

public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public UsersApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetUser_ReturnsErrorBody_WhenUserNotFound()
    {
        var response = await _client.GetAsync("/api/users/999");

        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);

        var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();

        Assert.NotNull(error);
        Assert.Equal("Ресурс не найден", error!.Message);
        Assert.Equal(404, error.StatusCode);
    }
}

public class ErrorResponse
{
    public bool Success { get; set; }
    public string Message { get; set; } = "";
    public int StatusCode { get; set; }
}
```

Это уже integration-тест настоящего middleware.

---

# **Как Работает `HttpClient` В Таких Тестах**

Важно: здесь `HttpClient` не ходит в интернет.

Он ходит в поднятое внутри теста приложение.

То есть:
- не на чужой сервер
- не в браузер
- не в localhost как внешний процесс

А в in-memory test host.

Поэтому тесты:
- быстрые
- достаточно надёжные
- близки к реальному поведению API

---

# **Что Можно Делать Через `HttpClient`**

## `GET`
```csharp
var response = await client.GetAsync("/api/tasks");
```

## `POST`
```csharp
var response = await client.PostAsJsonAsync("/api/tasks", new
{
    title = "Task 1"
});
```

## `PUT`
```csharp
var response = await client.PutAsJsonAsync("/api/tasks/1", new
{
    title = "Updated"
});
```

## `DELETE`
```csharp
var response = await client.DeleteAsync("/api/tasks/1");
```

---

# **Что Проверять В `HttpResponseMessage`**

```csharp
response.StatusCode
response.Headers
response.Content
```

Примеры:

```csharp
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(response.IsSuccessStatusCode);
```

Чтение тела:

```csharp
var text = await response.Content.ReadAsStringAsync();
```

или

```csharp
var body = await response.Content.ReadFromJsonAsync<MyDto>();
```

---

# **Когда Integration-Тест Особенно Полезен**

Пиши его, если хочешь проверить:
- реально ли маршрут доступен
- правильно ли биндингится request body
- реально ли middleware возвращает нужный JSON
- реально ли `CreatedAtAction` даёт верный статус
- реально ли конфигурация приложения живая

---

# **Частые Ошибки В Integration-Тестах**

## 1. Проверять только `200`
Слабый тест.

Лучше проверять ещё:
- тело ответа
- статус
- иногда заголовки

## 2. Тестировать слишком много сразу
Один тест = один сценарий.

## 3. Смешивать unit и integration
Если ты напрямую создаёшь сервис и не делаешь HTTP-вызов, это уже не integration API test.

## 4. Завязываться на глобальное состояние
Если приложение использует статические словари, тесты могут влиять друг на друга.

---

# **Минимальный Реалистичный Набор Integration-Тестов Для API**
Если у тебя есть CRUD API, хороший минимум:
1. `POST` valid request -> `201`
2. `POST` invalid request -> `400`
3. `GET by id` existing -> `200`
4. `GET by id` missing -> `404`
5. middleware error body -> правильный JSON

---

# **Хорошая Структура Теста**
```csharp
[Fact]
public async Task CreateTask_ReturnsCreated_WhenRequestIsValid()
{
    // Arrange
    var request = new
    {
        title = "Write tests",
        assigneeEmail = "user@example.com"
    };

    // Act
    var response = await _client.PostAsJsonAsync("/api/tasks", request);

    // Assert
    Assert.Equal(HttpStatusCode.Created, response.StatusCode);

    var body = await response.Content.ReadFromJsonAsync<TaskResponse>();
    Assert.NotNull(body);
    Assert.Equal("Write tests", body!.Title);
}
```

---

# **Чем Отличается От Тестов Через Mock**
Mock-тест:
- ты проверяешь сервис напрямую
- не поднимаешь приложение
- сам контролируешь зависимости

Integration API test:
- ты тестируешь весь HTTP pipeline
- не важно, как внутри устроены вызовы
- ты смотришь глазами клиента

